Análisis Exploratorío de Datos - Velocidad del Viento#

Descripción básica de los datos#

El conjunto de datos abarca 6574 registros diarios promediados, provenientes de un conjunto de 5 sensores para variables meteorológicas, todos integrados en una estación meteorológica. Dicha estación se situó en un área extensa y despejada, a una altura de 21 metros. La recolección de datos se extendió desde enero de 1961 hasta diciembre de 1978, abarcando un total de 17 años. Dentro de la información recopilada se incluyen las precipitaciones medias diarias, así como las temperaturas máxima y mínima y la temperatura mínima sobre la superficie del césped.

El dataset contiene 6574 filas y 9 Columnas del DATASET:

DATE (YYYY-MM-DD).

WIND: Average wind speed [nudos].

IND: First indicator value.

RAIN: Precipitation Amount (mm).

IND.1: Second indicator value.

T.MAX: Maximum Temperature (°C).

IND.2: Third indicator value.

T.MIN: Minimum Temperature (°C).

T.MIN.G: 09utc Grass Minimum Temperature (°C).

Para el desarrollo de este informe solo se han tomado en cuenta las variables DATE, WIND, RAIN, T.MAX, T.MIN y T.MIN.G; los indicadores mencionados previamente se excluyen del análisis teniendo en cuenta que la fuente en la que reposa el conjunto de datos estudiado, no proporciona una descripción o definición sobre qué se mide con dichos indicadores.
import pandas as pd
url = 'https://raw.githubusercontent.com/evgomez98/wind_speed/main/wind_dataset.csv'
df = pd.read_csv(url)
df.head()
DATE WIND IND RAIN IND.1 T.MAX IND.2 T.MIN T.MIN.G
0 1961-01-01 13.67 0 0.2 0.0 9.5 0.0 3.7 -1.0
1 1961-01-02 11.50 0 5.1 0.0 7.2 0.0 4.2 1.1
2 1961-01-03 11.25 0 0.4 0.0 5.5 0.0 0.5 -0.5
3 1961-01-04 8.63 0 0.2 0.0 5.6 0.0 0.4 -3.2
4 1961-01-05 11.92 0 10.4 0.0 7.2 1.0 -1.5 -7.5

Eliminamos las columnas correpondientes a las variables de los indicadores, como se mencionó de manera inicial:

df = df.drop(columns=["IND", "IND.1", "IND.2"])

Tambien es necesesario cambiar el formato de la columna ‘DATE’ a formato de fecha, como se muestra a continuación:

df['DATE'] = pd.to_datetime(df['DATE'])
print(df.head())
        DATE   WIND  RAIN  T.MAX  T.MIN  T.MIN.G
0 1961-01-01  13.67   0.2    9.5    3.7     -1.0
1 1961-01-02  11.50   5.1    7.2    4.2      1.1
2 1961-01-03  11.25   0.4    5.5    0.5     -0.5
3 1961-01-04   8.63   0.2    5.6    0.4     -3.2
4 1961-01-05  11.92  10.4    7.2   -1.5     -7.5
df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 6574 entries, 0 to 6573
Data columns (total 6 columns):
 #   Column   Non-Null Count  Dtype         
---  ------   --------------  -----         
 0   DATE     6574 non-null   datetime64[ns]
 1   WIND     6574 non-null   float64       
 2   RAIN     6574 non-null   float64       
 3   T.MAX    5953 non-null   float64       
 4   T.MIN    5900 non-null   float64       
 5   T.MIN.G  6214 non-null   float64       
dtypes: datetime64[ns](1), float64(5)
memory usage: 308.3 KB

El conjunto de datos contiene 6574 observaciones y 6 variables. La variable DATE indica la fecha de cada observación, mientras que las variables numéricas WIND, RAIN, T.MAX, T.MIN y T.MIN.G.

df.describe()
DATE WIND RAIN T.MAX T.MIN T.MIN.G
count 6574 6574.000000 6574.000000 5953.000000 5900.000000 6214.000000
mean 1969-12-31 12:00:00 9.796834 1.885169 13.339123 6.411678 2.736547
min 1961-01-01 00:00:00 0.000000 0.000000 -0.100000 -11.500000 -14.400000
25% 1965-07-02 06:00:00 6.000000 0.000000 9.600000 3.000000 -1.000000
50% 1969-12-31 12:00:00 9.210000 0.200000 13.300000 6.500000 3.000000
75% 1974-07-01 18:00:00 12.960000 2.000000 17.200000 10.000000 7.000000
max 1978-12-31 00:00:00 30.370000 67.000000 26.800000 18.000000 15.800000
std NaN 4.977272 4.030529 4.890546 4.637243 5.569175

Según el resumen, podemos observar que la variable WIND (velocidad del viento) tiene valores que van desde 0 hasta 30.37 nudos. La mayoría de los datos se encuentran en el rango de 6 a 12 nudos, ya que el 25% de los datos está por debajo de 6 y el 75% está por debajo de 12. La mediana de la velocidad del viento es de aproximadamente 9.21 nudos, lo que significa que la mitad de los datos están por encima de este valor y la otra mitad por debajo. Además, la media está cerca de 9.8 nudos.

En cuanto a la variable RAIN (cantidad de precipitación), vemos que la cantidad de lluvia varía desde 0 hasta 67 mm. La mayoría de los días tienen una cantidad de lluvia baja, ya que el 25% de los datos tienen una cantidad de lluvia de 0 mm y el 75% tienen menos de 2 mm. La mediana de la cantidad de lluvia es de 0.2 mm, lo que significa que la mitad de los días tienen una cantidad de lluvia superior a este valor y la otra mitad inferior.

Para las variables de temperatura, T.MAX (temperatura máxima) y T.MIN (temperatura mínima), vemos que los valores están en grados Celsius. La temperatura máxima varía desde -0.1°C hasta 26.8°C, mientras que la temperatura mínima varía desde -11.5°C hasta 18°C. La mediana de T.MAX es aproximadamente 13.3°C y la mediana de T.MIN es aproximadamente 6.5°C.

Las variables T.MIN.G (temperatura mínima en el pasto) y T.MIN (temperatura mínima) parecen tener valores similares, pero T.MIN.G tiende a ser un poco más baja en general.

Es importante mencionar que se require analizar sin se presentan valores faltantes (NA’s). A continuación procederemos a realizar un análisis detallado de los datos faltantes antes de continuar con el reporte.

Análisis de Datos Faltantes#

df.isnull().sum()
DATE         0
WIND         0
RAIN         0
T.MAX      621
T.MIN      674
T.MIN.G    360
dtype: int64

Una vez verificada la existencia de datos faltantes, visualizamos la ubicación de estos en el conjunto de datos:

import plotly.express as px

null_data = df.isnull().astype(int)
fig = px.imshow(null_data, 
                color_continuous_scale='Blues_r', 
                title='Mapa de Calor de Valores Nulos', 
                labels={'x': 'Columnas', 'y': 'Índices'},
                aspect='auto')
fig.update_layout(coloraxis_showscale=False) 

fig.show()

Se observan las 360 observaciones que tienen datos faltantes para la variable T.MIN.G, 621 para la variable T.MAX y 674 para la variable T.MIN.

Se realiza la imputación de datos, con la mediana, para proceder con el análisis y evitar incovenientes en el modelamiento de los datos.

df2 = df.copy()

df2['T.MAX'] = df2['T.MAX'].fillna(df2['T.MAX'].median())

df2['T.MIN'] = df2['T.MIN'].fillna(df2['T.MIN'].median())

df2['T.MIN.G'] = df2['T.MIN.G'].fillna(df2['T.MIN.G'].median())

Verificamos que la imputación se realizó correctamente:

df2.isnull().sum()
DATE       0
WIND       0
RAIN       0
T.MAX      0
T.MIN      0
T.MIN.G    0
dtype: int64

Visualización del resumen estadístico#

import plotly.graph_objects as go
fig = go.Figure()

fig.add_trace(go.Box(y=df2['WIND'], name='WIND'))
fig.add_trace(go.Box(y=df2['RAIN'], name='RAIN'))
fig.add_trace(go.Box(y=df2['T.MAX'], name='T.MAX'))
fig.add_trace(go.Box(y=df2['T.MIN'], name='T.MIN'))
fig.add_trace(go.Box(y=df2['T.MIN.G'], name='T.MIN.G'))

fig.update_layout(
    title="Box Plot de Variables en wind_s",
    yaxis_title="Valor",
    xaxis_title="Variable", 
     template='plotly_white', 
    width=800,  
    height=600
)

En concordancia con lo hallado en el resúmen estadístico, se visualizan los box plot, donde además de obtener una idea gráfica de dicho resúmen, obtenemos una idea preliminar de como podría lucir la distribución de nuestras variables. Además, podemos observar que las variables, a excepción de T.MAX, contienen datos atípicos en sus observaciones.

Análisis de tendencia#

from plotly.subplots import make_subplots

fig = make_subplots(rows=5, cols=1, row_heights=[6,6,6,6,6],
                    subplot_titles=("WIND", "RAIN", "T.MAX", "T.MIN", "T.MIN.G"))

fig.add_trace(go.Scatter(x=df2['DATE'], y=df2['WIND'], mode='lines', line=dict(color='blue')),
              row=1, col=1)
fig.add_trace(go.Scatter(x=df2['DATE'], y=df2['RAIN'], mode='lines', line=dict(color='red')),
              row=2, col=1)
fig.add_trace(go.Scatter(x=df2['DATE'], y=df2['T.MAX'], mode='lines', line=dict(color='green')),
              row=3, col=1)
fig.add_trace(go.Scatter(x=df2['DATE'], y=df2['T.MIN'], mode='lines', line=dict(color='purple')),
              row=4, col=1)
fig.add_trace(go.Scatter(x=df2['DATE'], y=df2['T.MIN.G'], mode='lines', line=dict(color='orange')),
              row=5, col=1)


fig.update_layout(
    title="Trend Plots de las variables asociadasa a la velocidad del viento",
    xaxis_title="",
    yaxis_title="",
     template='plotly_white', 
    showlegend=False,  
    width=800,  
    height=800
)
fig.show()

La grafica de tendencia de las variables muestra un comportamiento interesante de describir. Para la velocidad del viento, tenemos que a suimple vista no se precibe una tendencia o patrón que inidique comportamiento estacional. La lluvia es similar, muestra un comportamiento irregular en los periodos observados, sin una tendencia marcada. La temperatura maxima, temperatura mínima y temperatura mínima a nivel del césped tienen más similitud en qué, además de no mostrar una tendencia en a través del tiempo, parecen marcar un patrón repetitivo de manera periodica.

Histogramas de frecuencia#

from plotly.subplots import make_subplots
import numpy as np

variables = ["WIND", "RAIN", "T.MAX", "T.MIN", "T.MIN.G"]
colors = ['blue', 'red', 'green', 'purple', 'orange']

fig = make_subplots(rows=5, cols=1, row_heights=[6,6,6,6,6],
                    subplot_titles=variables)

for i, variable in enumerate(variables):
    hist_data = df[variable].dropna()  

    fig.add_trace(go.Histogram(x=hist_data, histnorm='probability density',
                               name=f'{variable} Histogram',
                               marker_color=colors[i]),
                  row=i+1, col=1)

    density = np.histogram(hist_data, bins=30, density=True)
    bin_centers = (density[1][1:] + density[1][:-1]) / 2
    fig.add_trace(go.Scatter(x=bin_centers, y=density[0],
                             mode='lines', line=dict(color='black'),
                             name=f'{variable} Density'),
                  row=i+1, col=1)

fig.update_layout(
    title_text="Histogramas de Frecuencia",
    showlegend=False,
    xaxis_title="",
    yaxis_title="", 
     template='plotly_white', 
    width=800,  
    height=800 
)


fig.show()

Apartir de los histogramas de frencuencia se puede observar que, la velocidad del viento (WIND) parece mostrar un sesgo hacia la derecha, de acuerdo con lo hallado en el resúmen, que la media es ligeramente mayor que la mediana, lo que indica una concentración de valores más altos. Además, hay una considerable dispersión en los datos de velocidad del viento, evidenciada por el amplio rango de valores observados. Similarmente, la cantidad de precipitación (RAIN) también muestra un sesgo hacia la derecha, aunque mucho más marcado y una dispersión notable en los datos, con un rango bastante amplio de valores.

En contraste, las distribuciones de las temperaturas máxima (T.MAX) parece estar más cerca de ser simétrica, con una dispersión moderada en sus datos. Sin embargo, a temperatura mínima (T.MIN) y la temperatura mínima en el pasto (T.MIN.G) exhiben una tendencia ligeramente más baja en comparación con T.MAX.

correlation_matrix = df2.iloc[:, 1:].corr()

fig = go.Figure(data=go.Heatmap(
    z=correlation_matrix.values,
    x=correlation_matrix.columns,
    y=correlation_matrix.columns,
    colorscale='Blues_r',
    reversescale=True,
    text=np.round(correlation_matrix.values, 2),
    hoverinfo='text'
))

fig.update_layout(
    title="Matriz de correlación",
    xaxis_title="",
    yaxis_title="",
    coloraxis_colorbar=dict(
        title="Correlation"
    ), 
    width=800,  
    height=500 
)

Se observa que la velocidad del viento (WIND) presenta una correlación negativa moderada con la temperatura máxima (T.MAX), lo que sugiere que a medida que la velocidad del viento aumenta, la temperatura máxima tiende a disminuir. Por otro lado, la correlación entre la cantidad de lluvia (RAIN) y la temperatura mínima (T.MIN) es baja y prácticamente nula, lo que indica una relación lineal débil entre estas dos variables. La temperatura mínima del césped (T.MIN.G) muestra una correlación positiva moderada con la temperatura mínima y una correlación prácticamente nula con la velocidad del viento. Además, se observa una correlación fuerte y positiva entre la temperatura mínima y la temperatura mínima del césped, lo que sugiere dependencia entre estas.

Histogrma de Frecuencias para la Velocidad del Viento#

fig = go.Figure()

fig.add_trace(go.Histogram(
    x=df['WIND'],
    histnorm='probability density', 
    name='Histogram', 
    marker_color='blue' 
))
fig.update_layout(template='plotly_white')

fig.show()

Medias móviles y efectos de suavización#

MA2 = df['WIND'].rolling(window=2).mean()
TwoXMA2 = MA2.rolling(window=2).mean()

MA4= df['WIND'].rolling(window=4).mean()
TwoXMA4 = MA4.rolling(window=2).mean()

MA3 = df['WIND'].rolling(window=3).mean()
ThreeXMA3 = MA3.rolling(window=3).mean()

df['MA2'] = MA2.dropna()
df['MA4'] = MA4.dropna()
df['MA3'] = MA3.dropna()

df['TwoXMA2'] = TwoXMA2.dropna()
df['TwoXMA4'] = TwoXMA4.dropna()
df['ThreeXMA3'] = ThreeXMA3.dropna()
fig = make_subplots(rows=3, cols=1, shared_xaxes=True, row_heights=[6,6,6],
                    subplot_titles=('2 day MA & 2X2 day MA', 
                                    '4 day MA & 2X4 day MA', 
                                    '3 day MA & 3X 3day MA'))

fig.add_trace(go.Scatter(x=df.index[:45], y=df['WIND'].iloc[:45], 
                         mode='lines', name='WIND', line=dict(color='blue')),
              row=1, col=1)
fig.add_trace(go.Scatter(x=df.index[:45], y=df['MA2'].iloc[:45], 
                         mode='lines', name='MA2', line=dict(color='red', dash='dash')),
              row=1, col=1)
fig.add_trace(go.Scatter(x=df.index[:45], y=df['TwoXMA2'].iloc[:45], 
                         mode='lines', name='2X MA2', line=dict(color='green', dash='dash')),
              row=1, col=1)

fig.add_trace(go.Scatter(x=df.index[:45], y=df['WIND'].iloc[:45], 
                         mode='lines', name='WIND', line=dict(color='blue')),
              row=2, col=1)
fig.add_trace(go.Scatter(x=df.index[:45], y=df['MA4'].iloc[:45], 
                         mode='lines', name='MA4', line=dict(color='red', dash='dash')),
              row=2, col=1)
fig.add_trace(go.Scatter(x=df.index[:45], y=df['TwoXMA4'].iloc[:45], 
                         mode='lines', name='2X MA4', line=dict(color='green', dash='dash')),
              row=2, col=1)

fig.add_trace(go.Scatter(x=df.index[:45], y=df['WIND'].iloc[:45], 
                         mode='lines', name='WIND', line=dict(color='blue')),
              row=3, col=1)
fig.add_trace(go.Scatter(x=df.index[:45], y=df['MA3'].iloc[:45], 
                         mode='lines', name='MA3', line=dict(color='red', dash='dash')),
              row=3, col=1)
fig.add_trace(go.Scatter(x=df.index[:45], y=df['ThreeXMA3'].iloc[:45], 
                         mode='lines', name='3X MA3', line=dict(color='green', dash='dash')),
              row=3, col=1)

fig.update_layout(title_text='Moving Averages',
                  template='plotly_white', 
                  width=800,
                  height=800
)

fig.show()

Se utilizan seis medias moviles para suavizar la serie temporal de la velocidad del viento co medias moviles de primer y segundo orden para ventanas de 2, 3 y 4 días. Se onbserva como se suaviza la serie en tanto que aumenta m y n.

Análisis de residuales#

from sklearn.linear_model import LinearRegression

trend_model = LinearRegression(fit_intercept=True)
trend_model.fit(np.arange(df.shape[0]).reshape((-1,1)), df['WIND'])
LinearRegression()
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
print('Trend model coefficient={} and intercept={}'.format(trend_model.coef_[0], trend_model.intercept_))
Trend model coefficient=-0.0001920374639126968 and intercept=10.427965624692735
df['residuals'] = np.array(df['WIND']) - trend_model.predict(np.arange(df.shape[0]).reshape((-1,1)))

Serie temporal anual#

df['DATE'] = pd.to_datetime(df['DATE'], format='%Y-%m-%d') 
df['Año'] = df['DATE'].dt.year

fig = px.line(df, x='DATE', y='residuals', color='Año',
              title='Gráfico de Serie Temporal por Año')

fig.update_layout(
    xaxis_title='Fecha',
    yaxis_title='Valor',
    template='plotly_white', 
    width=900,
    height=400
)
fig.show()

En busca de reconocer patrones, gráficamos la serie completa asignando colores a cada año. De este modo se puede observar, como se insinual un patrón estacional en cada año, donde la velocidad del viento parece alcanzar sus maximos al iniciar y termianr cada año.

Box plots anuales#

df['DATE'] = pd.to_datetime(df['DATE'], format='%Y-%m-%d')
df['Año'] = df['DATE'].dt.year

fig = px.box(df, x='Año', y='residuals',  color='Año',
             title='Boxplots de WIND por Año')

fig.update_layout(
    xaxis_title='Año',
    yaxis_title='WIND',
    template='plotly_white', 
    width=900,
    height=400
)

fig.show()

Se observa que la mediana a lo largo de los años, parece mantenerse relativamente estable, aunque fluctúa ligeramente, lo que indica que la intensidad típica del viento no varía mucho entre años. Por otro lado, el rango intercuartilico varía de un año a otro, lo que indica que la dispersión del viento cambia cada año. Como en los años 1975 y 1976 tienen una caja más pequeña, indicando menos variabilidad en esos años, sin embargo en 1962 y 1967 la variabilidad es mayor. En varios años se observan datos atípicos, mostrando que en estos, la velocidad del viento alcanzó valores muy altos.

Box plots semestrales#

df['Mes'] = df['DATE'].dt.month
df['Trimestre'] = df['Mes'].apply(lambda x: (x - 1) // 3 + 1)

fig = px.box(df, x='Trimestre', y='residuals',  color='Trimestre',
             title='Boxplots de WIND por Trimestre')

fig.update_layout(
    xaxis_title='Trimestre',
    yaxis_title='WIND',
    template='plotly_white', 
    width=900,
    height=400
)

fig.show()

Como se observó en la gráfica de la serie seccionada por años, el viento parece ser más variable en los trimestres primero y cuarto, con más valores atípicos en el segundo trimestre. Los trimestres segundo y tercero tienen una distribución más concentrada y menos dispersión en los valores de viento.

Box plots mensuales#

fig = px.box(df, x='Mes', y='residuals',  color='Mes',
             title='Boxplots de WIND por Mes')

fig.update_layout(
    xaxis_title='Mes',
    yaxis_title='WIND',
    template='plotly_white', 
    width=900,
    height=400
)

fig.show()

En los box plots mensuales se verifica el comportamiento que se venia analizando en los anteriores.

Prueba de Dickey-Fuller#

Para verificar la estacionariedad se aplica la prueba de Dickey-Fuller, con \(H_0\): La serie no es estacionaria.

from statsmodels.tsa.stattools import adfuller

adf_result = adfuller(df['WIND'], autolag='AIC')
print('p-valor del test ADF en La velocidad del viento:', adf_result[1])
p-valor del test ADF en La velocidad del viento: 2.0792341361651504e-13

Con un \(P-Value\)= 0,00 se rechaza \(H_0\). Es decir, la serie de tiempo de la velocidad del viento es estacionaria.

Descomposición de la serie Wind Speed#

import plotly.graph_objects as go
from plotly.subplots import make_subplots
from statsmodels.tsa.seasonal import seasonal_decompose

df.set_index('DATE', inplace=False)

result = seasonal_decompose(df['WIND'], model='additive', period=365)

fig = make_subplots(rows=4, cols=1, shared_xaxes=True, 
                    vertical_spacing=0.05,
                    subplot_titles=("Serie Original", "Tendencia", "Estacionalidad", "Residuo"))

fig.add_trace(go.Scatter(x=df.index, y=df['WIND'], mode='lines', name='Original'), row=1, col=1)
fig.add_trace(go.Scatter(x=df.index, y=result.trend, mode='lines', name='Tendencia'), row=2, col=1)
fig.add_trace(go.Scatter(x=df.index, y=result.seasonal, mode='lines', name='Estacionalidad'), row=3, col=1)
fig.add_trace(go.Scatter(x=df.index, y=result.resid, mode='lines', name='Residuo'), row=4, col=1)

fig.update_layout(
    title='Descomposición de la Serie de Tiempo WIND',
    xaxis_title='',
    yaxis_title='',
    height=800,
    template='plotly_white',
    showlegend=False
)
fig.update_xaxes(title_text="Fecha", row=4, col=1) 

fig.show()

La descomposicón muestra una tendencia que parece inciar de manera relativamente constante para luego caer en el año 1967 y en adelante mostrar fluctuciones a lo largo de los años restantes. Asimismo, la gráfica sugiere que el conjunto de datos presenta estacionalidad de manera anual.

Autocorrelación y Autocorrelación Parcial#

from matplotlib import pyplot as plt
from statsmodels.graphics.tsaplots import plot_acf, plot_pacf 

def plotds(xt, nlag=365, fig_size=(12, 10)):
    
    if not isinstance(xt, pd.Series):
         xt = pd.Series(xt)
    plt.figure(figsize=fig_size)
    layout = (2, 2)
    
    ax_xt = plt.subplot2grid(layout, (0, 0), colspan=2)
    ax_acf = plt.subplot2grid(layout, (1, 0))
    ax_pacf = plt.subplot2grid(layout, (1, 1))
    
    xt.plot(ax=ax_xt)
    ax_xt.set_title('Time Series')
    plot_acf(xt, lags=365, ax=ax_acf)
    plot_pacf(xt, lags=30, ax=ax_pacf)
    plt.tight_layout()
    
    return None
plotds(df['WIND'])
_images/922ec9af051e17287224b17c30b0882f0b8678c658c5c2f22deeb896ed0805cb.png

El primer diagnóstico sobre la gráfica ACF se trata del patrón sinusoidal que presenta, mostrando autocorrelación y sugiriendo estacionalidad en la serie de tiempo. Por otro lado, la función de autocorrelación parcial muestra cortes significativos en múltiples rezagos lo que sugiere que varios términos de autorregresión podrían ser relevantes para el modelo. Debido a la combinación de cortes en rezagos bajos y altos, es posible que sea necesario consideras realizar pruebas de modelos ARIMA con diferentes valores de p y q, también considerar modelos SARIMA que puedan capturar tanto los patrones de autorregresión como los patrones estacionales en los datos.